package edu.amm.neur;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.Semaphore;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Side;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import edu.amm.neur.MeshBoard.Tool;

/**
 * Контроллер основной формы приложения.
 * 
 * @author Иванов Илья
 * @since 2013-04-25
 *
 */
public class HopfieldController implements Initializable {
	
	@FXML
	private AnchorPane window;
	@FXML
	private AnchorPane drawPane;
	@FXML
	private AnchorPane comparePane;
	@FXML
	private MenuItem exit;
	@FXML
	private CheckMenuItem pencil;
	@FXML
	private CheckMenuItem eraser;
	@FXML
	private MenuItem clear;
	@FXML
	private MenuItem train;
	@FXML
	private MenuItem recover;
	@FXML
	private Label status;
	
	private MeshBoard drawBoard;
	private MeshBoard compareBoard;
	private FileChooser fileChooser = new FileChooser();
	
	private HopfieldNetwork network = new HopfieldNetwork(VECTOR_LENGTH);
	private State state;
	
	private Semaphore semaphore = new Semaphore(1, true);
	
	public void initialize(URL location, ResourceBundle resources) {
		initImages();
		setHandlers();
		
		pencil.setSelected(true);
		try {
			fileChooser.setInitialDirectory(new File("./").getCanonicalFile());
		} catch (IOException e) {}
		
		ready();
	}
	
	private void initImages() {
		drawBoard = new MeshBoard(ROWS, COLUMNS, MESH_STEP, MESH_WIDTH, MESH_COLOR, FILL_COLOR, true);
		drawPane.getChildren().add(drawBoard);
		
		compareBoard = new MeshBoard(ROWS, COLUMNS, MESH_STEP, MESH_WIDTH, MESH_COLOR, FILL_COLOR, false);
		comparePane.getChildren().add(compareBoard);
	}
	
	private void setHandlers() {
		pencil.selectedProperty().addListener(new ChangeListener<Boolean>() {
			
			public void changed(ObservableValue<? extends Boolean> observable,
				Boolean oldValue, Boolean newValue) {
				if (newValue) {
					eraser.setSelected(false);
					drawBoard.setTool(Tool.PENCIL);
				} else
					drawBoard.setTool(null);
			}
		});
		
		eraser.selectedProperty().addListener(new ChangeListener<Boolean>() {
			
			public void changed(ObservableValue<? extends Boolean> observable,
				Boolean oldValue, Boolean newValue) {
				if (newValue) {
					pencil.setSelected(false);
					drawBoard.setTool(Tool.ERASER);
				} else
					drawBoard.setTool(null);
			}
		});
		
		clear.setOnAction(new EventHandler<ActionEvent>() {
			
			public void handle(ActionEvent event) {
				drawBoard.clear();
			}
		});
		
		for (final MeshBoard board : new MeshBoard[] { drawBoard, compareBoard })
			board.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
				
				private ContextMenu contextMenu;
				
				public void handle(MouseEvent event) {
					if (event.getButton() != MouseButton.SECONDARY) {
						if (contextMenu != null)
							contextMenu.hide();
						return;
					}
					
					if (contextMenu == null)
						contextMenu = getContextMenu(board);
					contextMenu.show(board, Side.TOP, event.getX(), event.getY() + CONTEXT_MENU_HEIGHT);
				}
			});
		
		train.setOnAction(new EventHandler<ActionEvent>() {
			
			public void handle(ActionEvent event) {
				train();
			}
		});
		
		recover.setOnAction(new EventHandler<ActionEvent>() {
			
			public void handle(ActionEvent event) {
				recover();
			}
		});
		
		exit.setOnAction(new EventHandler<ActionEvent>() {
			
			public void handle(ActionEvent event) {
				Platform.exit();
			}
		});
	}
	
	private ContextMenu getContextMenu(final MeshBoard board) {
		ContextMenu contextMenu = new ContextMenu();
		
		MenuItem menuItem = new MenuItem("Открыть...");
		menuItem.setOnAction(new EventHandler<ActionEvent>() {
			
			public void handle(ActionEvent event) {
				open(board);
			}
		});
		contextMenu.getItems().add(menuItem);
		
		menuItem = new MenuItem("Сохранить");
		menuItem.setOnAction(new EventHandler<ActionEvent>() {
			
			public void handle(ActionEvent event) {
				save(board);
			}
		});
		contextMenu.getItems().add(menuItem);
		
		return contextMenu;
	}
	
	private void open(MeshBoard board) {
		File file = fileChooser.showOpenDialog(board.getScene().getWindow());
		if (file == null)
			return;
		
		boolean[] vector = ArraysIO.readBooleanArray(file);
		if (vector == null)
			return;
		
		board.drawVector(vector);
	}
	
	private void save(MeshBoard board) {
		File file = fileChooser.showSaveDialog(board.getScene().getWindow());
		if (file == null)
			return;
		
		boolean[] vector = board.getVector();
		ArraysIO.write(vector, file);
	}
	
	private void train() {
		List<File> files = fileChooser.showOpenMultipleDialog(train.getParentPopup().getScene().getWindow());
		if (files == null || files.isEmpty())
			return;
		
		busy("Обучение");
		
		final List<BipolarVector> vectors = new ArrayList<>();
		for (File file : files) {
			boolean[] booleanVector = ArraysIO.readBooleanArray(file);
			if (booleanVector != null && booleanVector.length == VECTOR_LENGTH)
				vectors.add(new BipolarVector(booleanVector));
		}
		
		Task<Void> task = new Task<Void>() {
			
			protected Void call() throws Exception {
				network.train(vectors);
				
				return null;
			}
		};
		task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
			
			public void handle(WorkerStateEvent event) {
				ready();
			}
		});
		Thread thread = new Thread(task);
		thread.setDaemon(true);
		thread.start();
	}
	
	private void recover() {
		busy("Восстановление");
		
		final boolean[] bools = drawBoard.getVector();
		Task<Void> task = new Task<Void>() {
			
			protected Void call() throws Exception {
				network.output(new BipolarVector(bools), new IterationCallback() {
					
					private int iter = 0;
					
					public void result(final BipolarVector vector) {
						if (vector == null)
							return;
						
						try {
							if (++iter > 1)
								Thread.sleep(TIMEOUT_MILLIS);
							semaphore.acquire();
							
							Platform.runLater(new Runnable() {
								public void run() {
									drawBoard.drawVector(vector.asBooleanVector());
									busy("Восстановление (" + iter + ") ");
									semaphore.release();
								}
							});
							
							semaphore.acquire();
						} catch (InterruptedException e) {}
						
						semaphore.release();
					}
				});
				
				return null;
			}
		};
		task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
			
			public void handle(WorkerStateEvent event) {
				ready();
			}
		});
		
		Thread thread = new Thread(task);
		thread.setDaemon(true);
		thread.start();
	}
	
	private void ready() {
		state = State.READY;
		state.apply(status, null);
		if (window.disableProperty().get())
			window.disableProperty().set(false);
	}
	
	private void busy(String message) {
		state = State.BUSY;
		state.apply(status, message);
		if (!window.disableProperty().get())
			window.disableProperty().set(true);
	}
	
	private static final int ROWS = 20;
	private static final int COLUMNS = 20;
	private static final int VECTOR_LENGTH = ROWS * COLUMNS;
	private static final int MESH_STEP = 15;
	private static final int MESH_WIDTH = 1;
	private static final Color MESH_COLOR = Color.GRAY;
	private static final Color FILL_COLOR = Color.BLACK;
	private static final double CONTEXT_MENU_HEIGHT = 60;
	private static int TIMEOUT_MILLIS = 500;
	
	private enum State {
		
		READY(Color.GREEN) {
			
			public void apply(Label status, String message) {
				status.setTextFill(messageColor);
				status.setText("Готово");
			}
		},
		
		BUSY(Color.RED) {
			
			public void apply(Label status, String message) {
				status.setTextFill(messageColor);
				status.setText(message + "...");
			}
		};
		
		protected Color messageColor;
		
		private State(Color messageColor) {
			this.messageColor = messageColor;
		}
		
		public abstract void apply(Label status, String message);
	}
}